This section shows you one way to make an asynchronous I/O call from a threaded application. The straightforward way to do this is to create a separate thread that makes the I/O call and then puts itself to sleep so that other threads in the application can continue to work while the I/O request is being handled. You would also provide a completion routine that wakes up the stopped thread when the I/O task is complete.
Figure 5 shows the problem with this approach. It is possible for the completion routine to execute before the thread puts itself in the stopped state. If this happens, the completion routine returns without doing anything because the thread is still running when the completion routine attempts to wake it up. Then the thread puts itself in the stopped state and stays there forever waiting for a completion routine that has already finished executing.
Figure 5 Using a completion routine to wake up a thread making an asynchronous I/O call
One solution to this problem is to create two threads, one to make the I/O call and the other to wake up the first thread. Figure 6 illustrates this process.
Figure 6 Using two threads to handle an asynchronous I/O call
As you can see in the figure, the thread making the I/O call creates a second thread (the wake-up thread) that is in the stopped state. The purpose of the completion routine is to start the wake-up thread. It doesn't actually make the thread ready to run but marks it as available to be ready to run. At the next scheduling opportunity, the wake-up thread is set to the ready-to-run state. At the following scheduling opportunity, if it is at the top of the queue, it begins to run and can wake up the I/O thread.
This scheme is guaranteed to work because there is no way that the I/O thread can still be awake when the completion routine attempts to wake it up. The wake-up thread can run only after the I/O thread has put itself to sleep. Listing 10 shows the code to implement this process.
Listing 10 Making an asynchronous I/O call with two threads
/* Set up parameter block */
struct ExtendedParamBlk {
/* PB must be first so that the file system can get the data. */
ParamBlockRec pb;
ThreadTaskRef theAppTask;
ThreadID theThread;
};
typedef struct ExtendedParamBlk ExtendedParamBlk;
typedef ExtendedParamBlk *ExtendedParamBlkPtr;
/* Routine prototypes. */
pascal void MyIOExampleThread (void);
pascal void DoWakeUpThread (ThreadID threadToWake);
void MyCompletionRoutine (void);
/* Completion routines are called with register A0 pointing to */
/* the parameter block. */
pascal ExtendedParamBlkPtr GetPBPtr(void) = {0x2E88};
/* move.l a0, (sp) */
/* A routine in the main thread that creates a thread to make an I/O call */
void DoKickOffAnIOThread (void)
{
ThreadID newCoopID;
OSErr theError;
theError = NewThread(kCooperativeThread,
(ThreadEntryProcPtr)MyIOExampleThread,
nil,
kDefaultThreadStackSize,
kNoCreationOptions,
nil,
&newCoopID);
if (theError)
DebugStr("\p Could not make cooperative I/O thread");
/* Return and let the I/O thread do its thing! */
}
/* The entry point for the code to make the I/O call */
pascal void MyIOExampleThread (void)
{
ThreadID wakeupThreadID, meThreadID;
ThreadTaskRef theAppRef;
ExtendedParamBlk myAsyncPB;
OSErr theError, theIOResult;
/* Get the ID of MyIOExampleThread. */
theError = MacGetCurrentThread(&meThreadID);
if (theError != noErr)
DebugStr("\pFailed to get the current thread ID");
/* Get the application's task reference. */
theError = GetThreadCurrentTaskRef(&theAppRef);
if (theError != noErr)
DebugStr("\Could not get our task ref");
/* Create a wake-up thread. */
theError = NewThread(kCooperativeThread,
(ThreadEntryProcPtr)DoWakeUpThread,
(void*)meThreadID,
kDefaultThreadStackSize,
kNewSuspend,
nil,
&wakeupThreadID);
if (theError != noErr)
DebugStr("\pFailed to create a cooperative thread");
/* Prepare for and make the I/O call */
myAsyncPB.pb.ioParam.ioCompletion = (ProcPtr)MyCompletionRoutine;
myAsyncPB.pb.ioParam.ioResult = 0;/* Initialize the result. */
myAsyncPB.pb.ioParam.ioNamePtr = nil; /* No name used here. */
myAsyncPB.pb.ioParam.ioVRefNum = -1;/* The boot drive. */
myAsyncPB.theThread = wakeupThreadID;
myAsyncPB.theAppTask = theAppRef;
PBFlushVol((ParmBlkPtr)&myAsyncPB, async);
/* Put I/O thread to sleep */
theError = SetThreadState(kCurrentThreadID, kStoppedThreadState,
kNoThreadID);
if (theError != noErr)
DebugStr ("\pFailed to put current thread to sleep");
/* Get the result of the I/O operation */
theIOResult = myAsyncPB.pb.ioParam.ioResult;
. . .
}
void MyCompletionRoutine (void)
{
ExtendedParamBlkPtr myAsyncPBPtr;
ThreadTaskRef theAppTaskRef;
ThreadID theThreadID;
ThreadState theThreadState;
OSErr theError;
/* Get the parameter block. */
myAsyncPBPtr = GetPBPtr();
/* Get the data. */
theAppTaskRef = myAsyncPBPtr->theAppTask;
theThreadID = myAsyncPBPtr->theThread;
/* See if the thread is stopped yet - just to be sure. */
theError = GetThreadStateGivenTaskRef(theAppTaskRef, theThreadID,
&theThreadState);
/* If we can get the thread state, go for it! */
if (theError == noErr)
{
/* If it's not stopped, something is wrong. */
if (theThreadState != kStoppedThreadState)
DebugStr("\pWake-up thread is in the wrong state!");
/* Should be sleeping, mark it for wake up! */
else
SetThreadReadyGivenTaskRef(theAppTaskRef, theThreadID);
}
}
/* The wake up thread wakes up the I/O thread */
pascal void DoWakeUpThread (ThreadID threadToWake)
{
OSErr theError;
theError = SetThreadState(threadToWake, kReadyThreadState,
kNoThreadID);
if (theError != noErr)
DebugStr("\pFailed to wake our thread");
/* We've done our deed, so just return quietly and let it run. */
}
The code in Listing 10 is long but can be broken up into discreet parts. The first thing it does is to set up the parameter block. The extended parameter block holds the parameter block for use by the file system and has fields to hold the thread task reference and thread ID of the wake-up thread for use by the completion routine. After the parameter block declaration are prototypes for the entry functions to the I/O thread and the wake-up thread, and for the completion routine that marks the wake-up thread as ready. The inline routine GetPBPtr retrieves the address of the parameter block for the completion routine from register A0.
The DoKickOffThread function uses the NewThread function to create the cooperative wake-up thread. The entry point to this thread is the MyIOExampleThread function.
The MyIOExampleThread function does several things. It uses MacGetCurrentThread to get and store its thread ID. Next it gets the thread task reference for the application. The completion routine needs the thread task reference to make any Thread Manager calls to a thread in this application context because during execution of the completion routine, there is no guarantee as to which application is the current context. Then the MyIOExampleThread function creates the wake-up thread with the NewThread function. It specifies the DoWakeUp function as the entry point to the routine and passes its own thread ID as a parameter to this function. Note that the kNewSuspend option creates the new thread in the stopped state.
Next, the MyIOExampleThread function prepares for the I/O call by setting up the address of the completion routine and the extended data the completion routine requires, including the thread ID of the wake-up thread and the thread task reference for the current application. The actual I/O call is an asynchronous file system command.
The last thing MyIOExampleThread does is to call SetThreadState to put itself in the stopped state. It passes the kNoThreadID constant as the last parameter to indicate that the Thread Manager should schedule the next available thread, rather than any particular thread.
IMPORTANT
It is always important to keep the main thread in the ready or running state, and the current example shows one of the reasons why. If the main thread has stopped itself, there may be no threads running at all after the current thread stops itself. The completion routine will return and mark the wake-up thread as available, but without a rescheduling call from the main thread or some other thread, the wake-up thread will remain marked as available but never ready or running.
When the asynchronous I/O call completes, it calls the completion routine to indicate that it has finished. The completion routine retrieves the parameter block and gets the thread task reference for the application and the thread ID of the wake-up thread. For good measure, it uses the GetThreadStateGivenTaskRef function to verify that the wake-up thread is indeed stopped. It passes the thread task reference and the thread ID of the wake-up thread to this function. It then marks the wake-up thread as ready at the next reschedule with the SetThreadReadyGivenTaskRef . Again, it passes the thread task reference and the thread ID of the wake-up thread to this function.
At the next reschedule, the wake-up thread is made ready to run and eventually it begins executing. The entry point to this thread is the DoWakeUpThread function, which is passed the thread ID of the thread to wake--in this case, the thread ID of the I/O thread. The DoWakeUpThread function calls SetThreadState to change the state of the I/O thread from stopped to ready.